<?php

namespace app\service;
use Elastic\Elasticsearch\Client;
use Elastic\Elasticsearch\ClientBuilder;
use Elastic\Elasticsearch\Exception\AuthenticationException;
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elastic\Elasticsearch\Exception\MissingParameterException;
use Elastic\Elasticsearch\Exception\ServerResponseException;
use Elastic\Elasticsearch\Response\Elasticsearch;
use Exception;
use Http\Promise\Promise;

/**
 * ElasticSearch搜索引擎服务 -- composer require elasticsearch/elasticsearch
 *  # ====================== Elasticsearch数据类型及其属性 =============
 *  核心类型	字符串类型	string,text,keyword
 *	整数类型	integer,long,short,byte
 *	浮点类型	double,float,half_float,scaled_float
 *	逻辑类型	boolean
 *	日期类型	date
 *	范围类型	range
 *	二进制类型	binary
 *  复合类型	数组类型	array
 *	对象类型	object
 *	嵌套类型	nested
 *  地理类型	地理坐标类型	geo_point
 *	地理地图	geo_shape
 *  特殊类型	IP类型	ip
 *	范围类型	completion
 *	令牌计数类型	token_count
 *	附件类型	attachment
 *	抽取类型	percolator
 *
 *  # ==================Mapping 支持属性 ======================
 *  enabled：仅存储、不做搜索和聚合分析
 *  index：是否构建倒排索引（即是否分词，设置false，字段将不会被索引）
 *  index_option：存储倒排索引的哪些信息 4个可选参数：docs：索引文档号  freqs：文档号+词频  positions：文档号+词频+位置，通常用来距离查询 offsets：文档号+词频+位置+偏移量，通常被使用在高亮字段  分词字段默认是positions，其他默认时docs
 *  norms：是否归一化相关参数、如果字段仅用于过滤和聚合分析、可关闭分词字段默认配置，不分词字段：默认{“enable”: false}，存储长度因子和索引时boost，建议对需要参加评分字段使用，会额外增加内存消耗
 *  doc_value：是否开启doc_value，用户聚合和排序分析对not_analyzed字段，默认都是开启，分词字段不能使用，对排序和聚合能提升较大性能，节约内存
 *  fielddata：是否为text类型启动fielddata，实现排序和聚合分析针对分词字段，参与排序或聚合时能提高性能，不分词字段统一建议使用doc_value
 *  store：是否单独设置此字段的是否存储而从_source字段中分离，只能搜索，不能获取值
 *  coerce：是否开启自动数据类型转换功能，比如：字符串转数字，浮点转整型
 *  multifields：灵活使用多字段解决多样的业务需求
 *  dynamic：控制mapping的自动更新
 *  data_detection：是否自动识别日期类型
 *  analyzer：指定分词器，默认分词器为standard analyzer
 *  boost：字段级别的分数加权，默认值是1.0
 *  fields：可以对一个字段提供多种索引模式，同一个字段的值，一个分词，一个不分词
 *  ignore_above：超过100个字符的文本，将会被忽略，不被索引
 *  include_in_all：设置是否此字段包含在_all字段中，默认时true，除非index设置成no
 *  null_value：设置一些缺失字段的初始化，只有string可以使用，分词字段的null值也会被分词
 *  position_increament_gap：影响距离查询或近似查询，可以设置在多值字段的数据上或分词字段上，查询时可以指定slop间隔，默认值时100
 *  search_analyzer：设置搜索时的分词器，默认跟analyzer是一致的，比如index时用standard+ngram，搜索时用standard用来完成自动提示功能
 *  similarity：默认时TF/IDF算法，指定一个字段评分策略，仅仅对字符串型和分词类型有效
 *  trem_vector：默认不存储向量信息，支持参数yes（term存储），with_positions（term+位置），with_offsets（term+偏移量），with_positions_offsets（term+位置+偏移量）对快速高亮fast vector highlighter能提升性能，但开启又会加大索引体积，不适合大数据量用
 * Class ElasticSearchService
 * @package App\Service
 */
class ElasticSearchService
{
    private static $ElasticSearchObj = null;
    //ElasticSearch版本
    private static $version = 7.9;

    /**
     * elasticsearch-7.9.3-windows-x86_64\elasticsearch-7.9.3\config 里面的elasticsearch.yml后面添加 重启elasticsearch服务
     * http.cors.enabled: true
     * http.cors.allow-origin: "*"
     * 启动elasticsearch-head-master才会成功
     * ElasticSearchService constructor.
     */
    private function __construct()
    {
    }

    static public function getInstance(): ?Client
    {
        if (empty(self::$ElasticSearchObj)) {
            $params = array(
                env('ELASTIC_HOST', '172.16.168.93:9200')
            );
            //->setHosts($params)
            try {
                self::$ElasticSearchObj = ClientBuilder::create()->build();
            } catch (AuthenticationException $e) {

            }
        }
        return self::$ElasticSearchObj;
    }

    /**
     * 精确查找
     * 案例 : $ccc = ElasticSearchService::searchTerm('first_name','abc','hahahe','hahah2');
     * @param $field
     * @param $value
     * @param string $index_name
     * @param string $index_type
     * @return array
     * @throws ClientResponseException
     * @throws ServerResponseException
     */
    static public function searchTerm($field, $value, string $index_name = 'data_test', string $index_type = 'index_type'): array
    {
        $params = [
            'index' => $index_name,
            'body' => [
                'query' => [
                    'constant_score' => [ //非评分模式执行
                        'filter' => [     //过滤器，不会计算相关度，速度快
                            'term' => [   //精确查找，不支持多个条件
                                $field => $value
                            ]
                        ]
                    ]
                ]
            ]
        ];
        if(self::$version < 8){
            $params['type'] = $index_type;
        }
        return self::getInstance()->search($params);
    }

    /**
     * 模糊查找
     * 案例 : $ccc = ElasticSearchService::searchLike('first_name','abc',['first_name','last_name','id','age'],'hahahe','hahah2',0,10);
     * @param $field
     * @param $value
     * @param $select
     * @param string $index
     * @param string $index_type
     * @param int $from
     * @param int $size
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws ServerResponseException
     */
    static public function searchLike($field, $value, $select, string $index = 'data_test', string $index_type = '_doc', int $from = 0, int $size = 2)
    {
        $params = [
            'index' => $index,
            'body' => [
                'query' => [
                    'bool' => [    //非评分模式执行
                        'should' => [
                            'match' => [
                                $field => '*' . $value . '*',
                            ],

                        ],
//                        'filter' => [
//                            'term' => [
//                                $field => '*' . $value . '*',
//                                ],
//                        ],
                    ]
                ],
                'size' => $size,
                '_source' => $select, // 这个就是select的意思后面加需要的字段
                // "aggs"=> [//聚合函数  https://blog.csdn.net/R_P_J/article/details/78376622  group by  这group by 应该再做一个方法才行
                //       "group_by_last_name"=>[//该名称可以随便起
                //              "terms"=>[ "field"=>'age' ]
                //       ]
                // ]
            ],
        ];
        if(self::$version < 8){
            $params['type'] = $index_type;
        }
        return self::getInstance()->search($params);
    }

    /**
     * 查询文档 (分页，排序，权重，过滤)
     * 实例:  $searchLikedoc: [['key'=>'first_key','val'=>'拉','boost'=>3],['key'=>'first_key','val'=>'拉','boost'=>3]]
     * boost 表示权重
     * 实例: $sort  ['age'=>['order'=>'desc']]
     * 表是 age字段 用倒序排序
     * 如： ElasticSearchService::searchLikedoc([['key'=>'first_key','val'=>'拉','boost'=>3],['key'=>'first_key','val'=>'拉','boost'=>3]],['age'=>['order'=>'desc']],'hahahe','hahah2');
     * @param array $searchLikedoc
     * @param array $sort
     * @param string $index_name
     * @param string $index_type
     * @param int $from
     * @param int $size
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws ServerResponseException
     */
    static public function searchLikedoc(array $searchLikedoc = [], array $sort = [], string $index_name = "test_ik", string $index_type = "goods", int $from = 0, int $size = 2)
    {

        $findArr = [];
        foreach ($searchLikedoc as $key => $value) {
            $findArr [] = ['match' => [$value['key'] => ['query' => $value['val'], 'boost' => empty($value['boost']) ? 3 : $value['boost']]]];
        }
        $params = [
            'index' => $index_name,
            'body' => [
                'query' => [
                    'bool' => [
                        'should' => $findArr
                    ],
                ],
                'sort' => $sort // ['age'=>['order'=>'desc']]
                ,
                'from' => $from,
                'size' => $size
            ]
        ];
        if(self::$version < 8){
            $params['type'] = $index_type;
        }
        return self::getInstance()->search($params);
    }


    /**
     * 创建索引  就是创建库 只能创建一次
     * 案例 : $ccc = ElasticSearchService::create_index('hahahe');
     * @param string $index_name 索引名--相当于mysql数据库名
     * @return array|mixed|string
     * @throws ClientResponseException
     * @throws ServerResponseException
     * @throws MissingParameterException
     */
    static public function create_index(string $index_name = 'test_ik')
    {
        // 只能创建一次
        $params = [
            'index' => $index_name,
            'body' => [
                'settings' => [
                    'number_of_shards' => 5,
                    'number_of_replicas' => 0
                ]
            ]
        ];
        try {
            return self::getInstance()->indices()->create($params);
        } catch (Exception $e) {
            $msg = $e->getMessage();
            return json_decode($msg, true);
        }
    }

    /**
     * 删除索引
     * 实例: ElasticSearchService::delete_index('hahahe');
     * @param string $index_name 索引名--相当于mysql数据库名
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function delete_index(string $index_name = 'test_ik')
    {
        $params = ['index' => $index_name];
        return self::getInstance()->indices()->delete($params);
    }

    /**
     * 创建文档模板 就是索引格式,
     * 就是创建 表  只用创建一次 目前没有修改功能
     * $ccccc   =  ['id' => ['type' => 'integer'],'first_name' => ['type' => 'text','analyzer' => 'ik_max_word' //这个是中文分词],'last_name' => ['type' => 'text','analyzer' => 'ik_max_word'],'age' => ['type' => 'integer']];
     * $ccc = ElasticSearchService::create_mappings('hahahe','hahah2',$ccccc);
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @param array $properties
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function create_mappings(string $index_name = 'test_ik', string $type_name = 'goods', array $properties = [])
    {
        $params = [
            'include_type_name' => true,
            'index' => $index_name,
            'body' => [
                $type_name => [
                    '_source' => [
                        'enabled' => true
                    ],
                    'properties' => $properties
                ]
            ]
        ];
        if(self::$version < 8){
            $params['type'] = $type_name;
        }
        return self::getInstance()->indices()->putMapping($params);
    }

    /**
     * 查看映射 就是  表的 文档模板
     * 实例: $ccc = ElasticSearchService::get_mapping(hahahe','hahah2');
     * @param string $type_name 文档名 -- 相当于mysql数据表名 -- ElasticSearch 7.X开始不支持索引类型--ElasticSearch 8.X不在有索引类型
     * @param string $index_name 索引名--相当于mysql数据库名
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws ServerResponseException
     */
    static public function get_mapping(string $type_name = 'goods', string $index_name = 'test_ik')
    {
        $params = [
            'index' => $index_name,
        ];
        if(self::$version < 8){
            $params['type'] = $type_name;
            $params['include_type_name'] = true; //在ElasticSearch 8.X版本type不在支持-- 小于6.X版本的支持多个type ---7.X只支持一个type类型
        }
        return self::getInstance()->indices()->getMapping($params);
    }


    /**
     * 添加文档 --就是添加每一行的数据
     * 实例 : $ccc = ElasticSearchService::add_doc(6,['id'=>123,'first_name'=>'abc','last_name'=>'abc','age'=>243], 'hahahe','hahah2');
     * @param integer $id --主键id---页可以在字段类型type设置的时候integer-页可以自定义
     * @param $doc --需要添加的内容
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function add_doc(int $id, $doc, string $index_name = 'test_ik', string $type_name = 'goods')
    {
        $params = [
            'index' => $index_name,
            'id' => $id,
            'body' => $doc
        ];
        if(self::$version < 8){
            $params['type'] = $type_name;
        }
        return self::getInstance()->index($params);
    }

    /**
     * 判断文档存在
     * 实例 : ElasticSearchService::exists_doc(3,'hahahe','hahah2');
     * @param int $id --主键id
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function exists_doc(int $id = 1, string $index_name = 'test_ik', string $type_name = 'goods')
    {
        $params = [
            'index' => $index_name,
            'id' => $id
        ];
        if(self::$version < 8){
            $params['type'] = $type_name;
        }
        return self::getInstance()->exists($params);
    }


    /**
     * 获取文档
     * 实例: $ccc = ElasticSearchService::get_doc(2,'hahahe','hahah2');
     * @param string $id --主键id
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function get_doc(string $id = '', string $index_name = 'test_ik', string $type_name = 'goods')
    {
        $params = [
            'index' => $index_name,
            'id' => $id
        ];
        if(self::$version < 8){
            $params['type'] = $type_name;
        }
        return self::getInstance()->get($params);
    }

    /**
     * 更新文档
     * 实例 :  $ccc = ElasticSearchService::update_doc(2,['id'=>123,'first_name'=>'是发送到发鬼地方二而asdfasfd7.8dsser','last_name'=>'是发送到发鬼地方二而asdfasfd7.8dsser','age'=>23],'hahahe','hahah2');
     * @param int $id
     * @param $doc --需要跟新的内容
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function update_doc($doc,int $id = 1, string $index_name = 'test_ik', string $type_name = 'goods')
    {
        // 可以灵活添加新字段,最好不要乱添加
        $params = [
            'index' => $index_name,
            'id' => $id,
            'body' => ['doc' => $doc]
        ];
        if(self::$version < 8){
            $params['type'] = $type_name;
        }
        return self::getInstance()->update($params);
    }

    /**
     * 删除文档
     * 实例: $ccc = ElasticSearchService::delete_doc(2, 'hahahe','hahah2');
     * @param int $id
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function delete_doc(int $id = 1, string $index_name = 'test_ik', string $type_name = 'goods')
    {

        $params = [
            'index' => $index_name,
            'id' => $id
        ];
        if(self::$version < 8){
            $params['type'] = $type_name;
        }
        return self::getInstance()->delete($params);
    }


    /**
     * 判断指定的索引名是否存在
     * @param $index_name -索引名--相当于mysql数据库名
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function isExistsIndex($index_name)
    {
        $params = [
            'index' => $index_name,
        ];
        return self::getInstance()->indices()->exists($params);
    }

    /**
     * 判断指定的索引的类型是否存在
     * @param $index_name -索引名--相当于mysql数据库名
     * @param $type_name -索引名--相当于mysql数据库名
     * @return bool
     */
    static public function isExistsType($index_name, $type_name)
    {
        $params = [
            'index' => $index_name,
            'type' => $type_name,
        ];
        return self::getInstance()->indices()->existsType($params);
    }


    /**
     * 删除内容
     * @param $index_name
     * @param $type_name
     * @param array $query
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function deleteByQuery($index_name, $type_name, array $query = [])
    {
        $params = [
            'index' => $index_name,
            'body' => [
                'query' => $query
            ],
        ];
        if(self::$version < 8){
            $params['type'] = $type_name;
        }
        return self::getInstance()->deleteByQuery($params);
    }

    /**
     * 根据模型的表结构创建索引和索引类型
     * @param $dataModel -框架模型--models下面的
     * @param string $index_name -索引名--相当于mysql数据库名
     * @param string $type_name -索引类型--相当于mysql数据表名
     * @return bool|mixed|string
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    static public function createLibraryTable($dataModel, string $index_name="kg_server", string $type_name="bsl_server")
    {
        try{
            //判断索引是否存在
            $is_elastic_index = self::isExistsIndex($index_name);
            if (!$is_elastic_index) {
                //创建索引
                self::create_index($index_name);
            }
            //判断索引类型是否存在
            $is_elastic_type = self::isExistsType($index_name, $type_name);
            if (!$is_elastic_type) {
                //获取表字段和类型
                $properties= [];
                $getTableColumns = $dataModel->getTableColumns();
                //创建索引类型 -- 'analyzer' => 'ik_max_word'--如果没有安装用就会报错
                foreach ($getTableColumns as $key=>$value){
                    $getTableColumnsType = $dataModel->getTableColumnsType($value);
                    if($value != "id"){
                        $getTableColumnsType = "text";
                    }
                    $properties[$value] =['type' => $getTableColumnsType];
                }
                self::create_mappings($index_name, $type_name, $properties);
            }
            return true;
        }catch (\Exception $e){
            $msg = $e->getMessage();
            return json_decode($msg, true);
        }
    }

}
